MCP Server 如何避免把連線或 tool listing 的成功誤當成全部 Tool 的授權?
一個客服 Agent 連到 MCP Server,Server 列出 read_ticket、update_ticket、delete_ticket。連線已通過 OAuth,工程師便認為三者都可用。實際上,連線 authentication 只證明 caller 能到達受保護的 MCP resource;它不代表這個 Agent 在本 Task 能刪除任何 ticket。
這是 Agent-specific 的 protocol 邊界問題:模型根據動態 tool list 規劃多步行為,且同一個 session 可能代表不同 User 或 Task。Server 不能把 session 的成功當成所有未來 side effect 的永久 grant。
Alice 只獲准查詢 ticket。MCP Server 仍把三個工具列出,Agent 受 ticket 內容誘導呼叫 delete_ticket。若 server 只驗證 token signature,不驗證 tool name、resource 與 scope,刪除成功。反過來,若只過濾清單,攻擊者可直接送一個手工 JSON-RPC tools/call,繞過 UI 清單。
截至 2026-08-14 查核的 MCP 2025-11-25 Authorization 規格指出:HTTP authorization optional;支援時 MCP Server 是 OAuth resource server,必須驗證 token 是發給自己的 resource,且不得接受或轉送其他 token。token scope 不足時可回 403 insufficient_scope。這些是 transport/resource 層要求,不是 per-tool 業務授權的替代品。
tools/list 是 discovery,tools/call 才是 execution。可採三層控制:連線/受眾驗證、清單動態過濾、每次 call 的 operation/resource/task decision。清單過濾降低誤用與模型暴露面,但 execution PEP 必須能拒絕手工 call。
OAuth handshake 成功後把所有 tools 暴露;Server 只驗證 token 的 issuer/signature,或把 client token passthrough 給 downstream API。
Gateway 驗證 MCP resource audience 與 actor/subject context。tools/list 依 task grant 過濾 read/update/delete;tools/call 再把 tool name、arguments、resource 與資料敏感度送到 PDP。MCP Server 不接受非自身 audience 的 token,也不把 token 原封不動轉送下游;下游使用受限的新 credential 或 server-side identity。
MCP Client/Agent、Gateway、MCP Server、PDP、下游 Tool/Resource 各自驗證。tool description 和 tool result 都是可能影響計畫的不可信資料。
Client 以 canonical MCP Server URI 作 OAuth resource;Server 驗證 access token audience。Gateway 再保留 subject、actor、task_id 和 operation。MCP 規格要求每個 HTTP request 帶 Authorization header,不能把 token 放 query string。
PDP 對 tools/call(name="delete_ticket", arguments={"id":"T-9"}) 作決策。若 scope 只有 tickets:read,回 403;若 token 無效或 audience 錯誤,回 401。即使 delete_ticket 不在 list,也要對手工 call deny。

allowed = {"read_ticket"}
listed = [name for name in ["read_ticket", "update_ticket", "delete_ticket"] if name in allowed]
def call(name, token_audience, server_audience):
if token_audience != server_audience:
return 401
if name not in allowed:
return 403
return 200
assert listed == ["read_ticket"]
assert call("read_ticket", "mcp://support", "mcp://support") == 200
assert call("delete_ticket", "mcp://support", "mcp://support") == 403
assert call("read_ticket", "mcp://other", "mcp://support") == 401
print("list-filter=ok call-allow=ok call-deny=ok audience-deny=ok")
tools/list 過濾是 UX/減少暴露面控制,不是唯一 enforcement。tools/call 必須重新驗證 operation、resource、Task 與 scope。Day 18 進一步問:同一個 read_ticket 到底依固定 Role、動態 Context,還是本次 Task 來決定?